import { select, Selection } from 'd3-selection';
import L, { LatLng, LeafletMouseEvent, Point } from 'leaflet';
import Transform from '../geo/transform';
import Feature from '../feature/feature';
import PointsLayer from '../layer/pointsLayer';
import Mode from '../mode/mode';
import { Behave, FeatureInstance, FeatureJson, FeatureType, ModeType, StackItem } from '../types';
import History from '../core/history';
import EventBus from '../util/eventBus';
import Layer from '../layer/layer';
import LinesLayer from '../layer/linesLayer';
import NodesLayer from '../layer/nodesLayer';
import PolygonsLayer from '../layer/polygonsLayer';
import Vertex from '../feature/vertex';
import Line from '../feature/line';
import Polygon from '../feature/polygon';
import { line } from 'd3-shape';
import { getAnotation, debounced } from '../util/util';
import turf_bboxClip from '@turf/bbox-clip';
import turf_booleanDisjoint from '@turf/boolean-disjoint';
import { lineString as turfLineString, point as turfPoint, polygon as turfPolygon } from '@turf/helpers';
import { BBox2d, Position } from '@turf/helpers/dist/js/lib/geojson';

enum DragEventStep {
    default,
    down,
    move,
    up,
}
/**
 * 图画类，负责对地图上的各个图形进行管理
 */
class Graphic {
    /**
     * 地图
     */
    map: L.Map;
    /**
     * 模式
     */
    mode: Mode;
    /**
     * 坐标变换对象
     */
    transform: Transform;
    /**
     * 事件注册中心
     */
    eventBus: EventBus;
    /**
     * 未保存的数据记录
     */
    history: History;
    /**
     * 总的svg画布选择器
     */
    // @ts-ignore
    surface: Selection<HTMLElement, unknown, null, undefined>;
    /**
     * 所有点组成的图层
     */
    // @ts-ignore
    layers: {
        [featureType in FeatureType]: Layer;
    } = {};
    /**
     * mousedown 选中要素后，变为1,mousemove时变成2，mouseup变成2,因为选中要素的事件被屏蔽，也会触发mapclick事件重置dragstep
     */
    dragStep: number = 0;
    /**
     * 记录拖到时，鼠标所在的位置
     */
    mouseStartPos: LatLng = L.latLng(0, 0);
    /**
     * 记录拖动时，要素所在的位置
     */
    featureStartPos: LatLng = L.latLng(0, 0);

    debouncedClearFeature: Function;

    constructor(map: L.Map) {
        this.map = map;
        this.transform = new Transform(map);
        this.mode = new Mode();

        this.eventBus = new EventBus();
        this.history = new History(this.eventBus, {
            getActualFeature: this.getActualFeature.bind(this),
            transformFeature: this.transformFeature.bind(this),
            wait: this.wait.bind(this),
        });

        // 拖动时，延迟清理地图范围外得要素，减少重复渲染的次数
        this.debouncedClearFeature = debounced(this.removeOutViewFeatures.bind(this), 3000, false);

        this._createSurface();
        this._createLayers();

        this.showCacheFeature();

        this.addEventListener();
    }

    /**
     * svg画布
     */
    _createSurface() {
        // 叠加svg画布
        const svgOverlay = L.svg({ padding: 0 });
        this.map.addLayer(svgOverlay);

        // 获取svg的Dom元素，设置id属性作为唯一标识
        // @ts-ignore
        const svg = svgOverlay._container;
        svg.setAttribute('id', 'seditor');
        svg.setAttribute('pointer-events', 'auto');

        // 将dom元素转为d3的dom对象
        this.surface = select(svg).classed('seditor mode-browse', true);
        this.surface.append('defs');
        this.updateSvgClipPath();

        this.surface.selectAll('g').attr('clip-path', 'url(#seditorClipPath)');
    }

    /**
     * 更新画布容器的裁切范围（dom不会减少）
     */
    updateSvgClipPath() {
        const coordinates = this.getMapCoordinates();
        // 左上 右上 右下 左下 左上
        const pixes: [number, number][] = [
            [coordinates[0], coordinates[1]],
            [coordinates[2], coordinates[1]],
            [coordinates[2], coordinates[3]],
            [coordinates[0], coordinates[3]],
            [coordinates[0], coordinates[1]],
        ];

        const d = line()(pixes);

        this.surface.select('defs').select('#seditorClipPath').remove();
        this.surface.select('defs').append('clipPath').attr('id', 'seditorClipPath').append('path').attr('d', d);
    }

    /**
     * 创建各个要素图层
     */
    _createLayers() {
        // 准备面，线，点绘图图层组
        const data: { classname: string }[] = [
            { classname: 'layer-polygons' },
            { classname: 'layer-lines' },
            { classname: 'layer-nodes' },
            { classname: 'layer-points' },
        ];

        this.surface
            .select('g')
            .classed('layers', true)
            .selectAll('g')
            .data(data)
            .enter()
            .append('g')
            .attr('class', function (d) {
                return d.classname;
            });

        const layerOption = {
            map: this.map,
            surface: this.surface,
            transform: this.transform,
            mode: this.mode,
            eventBus: this.eventBus,
            getAllActualFeatures: this.getAllActualFeatures.bind(this),
            addHistory: this.addHistory.bind(this),
            changeMode: this.changeMode.bind(this),
            featureMousedown: this.featureMousedown.bind(this),
            remove: this.removeFeature.bind(this),
            transformFeature: this.transformFeature.bind(this),
            clipPath: this.clipPath.bind(this),
            changeFeatureId: this.changeFeatureId.bind(this),
        };

        const pointsLayer = new PointsLayer(layerOption);

        this.layers['point'] = pointsLayer;

        const nodesLayer = new NodesLayer(layerOption);
        this.layers['node'] = nodesLayer;
        const linesLayer = new LinesLayer({ ...layerOption, nodesLayer });
        this.layers['line'] = linesLayer;

        const polygonsLayer = new PolygonsLayer({ ...layerOption, nodesLayer });
        this.layers['polygon'] = polygonsLayer;
    }

    /**
     * 进入标绘模式
     * @param featureType
     */
    draw(featureType: FeatureType) {
        if (this.mode.getFeatureType() !== featureType) {
            this.mode.enter('draw', featureType);
        } else {
            this.mode.exit();
        }
    }

    changeMode(modeType: ModeType, feature: Feature) {
        // 选中模式不需要传featureType,不然会影响下一次点击标绘
        let featureType;
        if (modeType === 'draw') {
            featureType = feature.type;
        } else if (modeType === 'select') {
            this.toTopDom(feature);
        }

        this.mode.enter(modeType, featureType, feature);
    }

    addEventListener() {
        this.map

            .on('mousemove', this.mousemove, this)
            .on('click', this.mapClick, this)
            .on('mouseup', this.mapMouseup, this)
            .on('moveend', this.mapMove, this);
    }

    mapClick(e: LeafletMouseEvent) {
        let isClickFeatureInner = false;
        if (this.mode.selectedFeature && this.mode.selectedFeature.type === 'polygon') {
            const featureBounds = this.mode.selectedFeature.getBounds();
            isClickFeatureInner = featureBounds.contains(e.latlng) ? true : false;
        }
        if (isClickFeatureInner) {
            return;
        }
        if (this.dragStep === DragEventStep.up) {
            this.dragStep = DragEventStep.default;
            return;
        }
        const modeType = this.mode.getType();
        if (modeType !== 'draw') {
            this.mode.enter('browse');
            return;
        }
        const featureType = this.mode.getFeatureType()!;
        this.layers[featureType].draw(e.latlng);
    }

    mousemove(e: LeafletMouseEvent) {
        // 选中要素拖拽移动鼠标
        this.dragMove(e);
        // 绘制要素过程中移动鼠标
        this.drawMove(e);
    }

    drawMove(e: LeafletMouseEvent) {
        const { latlng } = e;
        const type = this.mode.getType();
        const featureType = this.mode.getFeatureType();

        if (type != 'draw' || featureType == 'point') {
            return;
        }

        this.layers[featureType!].drawMove(latlng);
    }
    mapMove() {
        this.updateSvgClipPath();

        this.debouncedClearFeature();
        Object.keys(this.layers).forEach((layerType) => {
            const layer = this.layers[<FeatureType>layerType];
            layer.redraw();
        });
    }

    featureMousedown(e: MouseEvent, feature: Feature) {
        if (this.mode.getType() === 'draw') {
            return;
        }

        this.dragStep = DragEventStep.down;
        this.map.dragging.disable();

        const latLngArr = this.transform.containerPointToLatLng(new L.Point(e.offsetX, e.offsetY));
        this.mouseStartPos = new L.LatLng(latLngArr[0], latLngArr[1]);
        this.featureStartPos = feature.latLngs[0][0];

        this.changeMode('select', feature);
    }

    /**
     * 拖动事件，拖动时，被拖动的要素事件会被屏蔽
     * @param e
     * @returns
     */
    dragMove(e: LeafletMouseEvent) {
        if (this.dragStep === DragEventStep.default || this.dragStep === DragEventStep.up) {
            // 没有点击要素
            return;
        }
        const latlng = e.latlng;
        const { selectedFeature } = this.mode;

        if (!selectedFeature || selectedFeature.type === 'line' || selectedFeature.type === 'polygon') {
            // todo 线和面暂时不允许拖动
            return;
        }

        const features =
            selectedFeature.type === 'node' ? (selectedFeature as Vertex).relateFeatures : [selectedFeature];
        this.addHistory({ annotation: getAnotation('add base '), features }, true);

        this.changeMode('drag', selectedFeature);

        if (this.dragStep === DragEventStep.down) {
            this.eventBus.emit('dragstart', selectedFeature.instance(), e.originalEvent);
        }
        this.dragStep = DragEventStep.move; // 拖动中
        const { lat, lng } = latlng;
        const { lat: mouseStartLat, lng: mouseStartLng } = this.mouseStartPos;
        const { lat: featureStartLat, lng: featureStartLng } = this.featureStartPos;
        // 鼠标点击位置和marker经纬度的偏移量
        const offsetLat = lat - mouseStartLat;
        const offsetLng = lng - mouseStartLng;
        const newLatlng = new L.LatLng(featureStartLat + offsetLat, featureStartLng + offsetLng);
        selectedFeature.changeLatlng(newLatlng, [0], 0);
    }

    /**
     * 容器内的mouseup都会触发
     * @param e
     * @returns
     */
    mapMouseup(e: LeafletMouseEvent) {
        this.map.dragging.enable();

        const selectFeature = this.mode.selectedFeature;

        if (!selectFeature) {
            return;
        }

        // 从拖动模式进入选中模式
        this.changeMode('select', selectFeature);

        if (this.dragStep === DragEventStep.move) {
            // 要素被拖动

            if (selectFeature.type === 'node') {
                (selectFeature as Vertex).dragEnd();
            }
            // 拖动后，保存到历史记录
            const features = selectFeature.type === 'node' ? (selectFeature as Vertex).relateFeatures : [selectFeature];
            this.addHistory({
                annotation: getAnotation('drag end ', selectFeature),
                features,
            });

            // 抛出拖动事件
            this.eventBus.emit('dragend', selectFeature.instance(), e.originalEvent);
            this.dragStep = DragEventStep.up;
        } else {
            this.dragStep = DragEventStep.default;
        }
    }

    /**
     * 添加历史记录，标绘/拖动/删除/添加中间点
     * @param behave
     * @param isBase  是否是基础要素，如果不是就判断history里是否存在基础要素，如果不存在，则添加一个空经纬度的基础要素
     * 撤销到基类要素时，如果经纬度是空则删除此要素，如果有经纬度，则回退到此经纬度的位置
     * 拖动前 / 删除前 / 添加中间点 / 交差点  都需要手动添加基类信息
     */
    addHistory(behave: Behave, isBase: boolean = false) {
        const { features } = behave;
        if (isBase) {
            const featuresJson = features.map((item) => item.clone());
            this.addBaseStackItem({ ...behave, featuresJson });
        } else {
            const nullLatLngJson = features.map((item) => {
                return { ...item.clone(), latLngs: [] };
            });
            this.addBaseStackItem({ ...behave, featuresJson: nullLatLngJson });
            this.history.add(behave);
        }
    }

    /**
     * 判断behave中的features是否存在基类，如果不存在则添加基类
     * @param behave
     */
    addBaseStackItem(behave: Behave) {
        const { featuresJson, annotation } = behave;

        featuresJson!.forEach((json) => {
            if (!this.history.contain(json.id)) {
                //如果不存在此要素信息，先添加个基类信息，作为最后一次撤销后的信息
                this.history.addStackItem({
                    featureJson: json,
                    annotation,
                    isBase: true,
                });
            }
        });
    }

    /**
     * 历史记录要求重新渲染某要素
     * @param stackItem
     */
    wait(stackItems: StackItem[]) {
        stackItems.forEach((stackItem) => {
            // 撤销到基类要素时，删除此要素
            const feature = stackItem.featureJson;
            if (stackItem.isBase && feature.latLngs.length === 0) {
                const { type, id } = feature;
                this.removeFeature(type, id);
            } else {
                this.replaceFeature(feature);
            }
        });
    }

    /**
     * 替换要素，撤销操作，用history里的要素替换layer里保存的要素
     * @param feature 需要替换的要素
     */
    replaceFeature(feature: FeatureJson) {
        const layerInnerFeature = this.transformFeature(feature.type, feature.id);
        // if (layerInnerFeature && layerInnerFeature.type === 'node') {
        //     const relateFeatures = this.recoverRelateFeature(feature);
        //     if (relateFeatures) {
        //         (layerInnerFeature as Vertex).relateFeatures = relateFeatures;
        //     }
        //     layerInnerFeature.latLngs = feature.latLngs;
        //     // 重绘顶点
        //     layerInnerFeature.redraw();
        //     // 重绘顶点的关联要素
        //     (layerInnerFeature as Vertex).redrawRelateFeature();
        // } else
        if (layerInnerFeature && layerInnerFeature.type === 'point') {
            layerInnerFeature.latLngs = feature.latLngs ? [feature.latLngs as LatLng[]] : [[]];
            layerInnerFeature.redraw();
        } else {
            // 不能在render关联要素后再removeFeature，会把新渲染的关联顶点删除掉
            if (layerInnerFeature) {
                const { type, id } = layerInnerFeature;
                this.removeFeature(type, id);
            }
            // this.renderRelateFeature(feature);
            this.layers[feature.type].render(<FeatureJson[]>[feature]);
        }
    }

    /**
     * 删除要素
     * @param feature 需要删除的要素
     */
    removeFeature(type: FeatureType, id: string): Feature | undefined {
        return this.layers[type].remove(id);
    }

    removeAllFeature() {
        const allFeatures = this.getAllActualFeatures();
        allFeatures.forEach((feature, key) => {
            this.removeFeature(feature.type, feature.id);
        });
    }

    /**
     * 调用过render后，渲染历史缓存数据
     */
    showCacheFeature() {
        const unSaved = this.history.getUnSavedFeatures();
        if (unSaved.length) {
            const features = unSaved
                .filter((item) => item.operate !== 'delete')
                .map((item) => item.feature)
                .map((item) => {
                    return JSON.parse(JSON.stringify(item));
                });
            this.render(features);
        }
    }

    /**
     * FeatureJson 转 Feature
     * @param json
     */
    transformFeature(type: FeatureType, id: string): Feature | undefined {
        return this.layers[type].features.get(id);
    }

    changeFeatureId(oldId: string, newId: string): Feature | undefined {
        const allFeatures = this.getAllActualFeatures();
        const feature = allFeatures.get(oldId);
        if (feature) {
            this.layers[feature.type].features.delete(oldId);
            feature.id = newId;
            this.layers[feature.type].features.set(newId, feature);
        }
        return feature;
    }

    /**
     * 把给定要素的dom置顶
     * @param feature
     */
    toTopDom(feature: Feature | FeatureJson) {
        const featureDom = document.getElementById('#' + feature.id);
        if (featureDom) {
            featureDom.parentNode?.append(featureDom);
        }

        if (feature.type === 'line' || feature.type === 'polygon') {
            // 如果是线和面，把顶点也都置顶
            (feature as Line | Polygon).relateNodeIds.flat().forEach((id) => {
                const node = this.layers['node'].features.get(id);
                if (node) {
                    this.toTopDom(node);
                }
            });
        }
    }

    /**
     * 获取地图容器范围,padding 20
     * @returns  minX, minY, maxX, maxY
     */
    getMapCoordinates() {
        let northEast = this.transform.project(this.map.getBounds().getNorthEast());
        let southWest = this.transform.project(this.map.getBounds().getSouthWest());

        const bbox: BBox2d = [southWest[0] - 20, northEast[1] - 20, northEast[0] + 20, southWest[1] + 20];
        return bbox;
    }

    /**
     * 当要素范围超出地图容器时，切割要素，返回新的经纬度数组
     * @param latLngs
     * @returns
     */
    clipPath(latLngs: LatLng[][]): LatLng[][] {
        const res: LatLng[][] = [];
        const mapBounds = this.map.getBounds();

        for (const latLngsItem of latLngs) {
            const c = mapBounds.contains(L.latLngBounds(latLngsItem));
            if (!c) {
                const coordinates: number[][] = latLngsItem.map((latLng) => {
                    const point = this.map.project(latLng).subtract(this.map.getPixelOrigin());
                    return [point.x, point.y];
                });

                // TODO 直接用经纬度数据组可以吗
                const line = turfLineString(coordinates);

                const bbox = this.getMapCoordinates();
                const clipped = turf_bboxClip(line, bbox);
                const arr = <number[]>clipped.geometry.coordinates.flat(Infinity);
                if (arr.length === 0) {
                    return [];
                }

                const positions: Position[] = [];
                for (let i = 0; i < arr.length / 2; i++) {
                    positions.push([arr[i * 2], arr[i * 2 + 1]]);
                }

                const latLngArr = positions.map((item) => {
                    return this.map.unproject(new L.Point(item[0], item[1]).add(this.map.getPixelOrigin()));
                });
                res.push(latLngArr);
            } else {
                res.push(latLngsItem);
            }
        }

        return res;
    }

    /**
     * 要素定位
     * @param type
     * @param id
     * @returns 定位的要素
     */
    fitFeature(type: FeatureType, id: string) {
        const feature = this.layers[type].features.get(id);
        if (feature) {
            const bounds = feature.getBounds();
            this.map.flyToBounds(bounds);
            this.changeMode('select', feature);
            return feature;
        } else {
            return undefined;
        }
    }

    /**
     * 渲染视野范围内的要素
     * @param features
     */
    render(features: FeatureJson[]): Feature[] {
        const points = this.getNoRenderFeatures(features, 'point');
        const resPoints = (this.layers['point'] as PointsLayer).render(points);

        const lines = this.getNoRenderFeatures(features, 'line');
        const resLines = (this.layers['line'] as LinesLayer).render(lines);

        const polygons = this.getNoRenderFeatures(features, 'polygon');
        const resPolygons = (this.layers['polygon'] as PolygonsLayer).render(polygons);

        const results = resPoints.concat(resLines).concat(resPolygons);

        return results;
    }

    /**
     * 获取还没有渲染的features
     * @param features
     * @param type
     * @returns
     */
    getNoRenderFeatures(features: FeatureJson[], type: FeatureType) {
        const noRenders = features.filter((item) => {
            return item.type === type;
        });
        return noRenders;
    }

    /**
     * 移除不在视野范围内且没有保存的要素
     */
    removeOutViewFeatures() {
        // 被修改的要素id集合
        const unSavedIds: string[] = this.history.getUnSavedFeatures().map((item) => item.feature.id);
        // 地图上所有的要素
        const allFeatures = this.getAllActualFeatures();
        // 地图四至范围
        const coordinates = this.getMapCoordinates();
        const pixes: number[][][] = [
            [
                [coordinates[0], coordinates[1]],
                [coordinates[2], coordinates[1]],
                [coordinates[2], coordinates[3]],
                [coordinates[0], coordinates[3]],
                [coordinates[0], coordinates[1]],
            ],
        ];
        // 地图四个点组成的多边形
        const box = turfPolygon(pixes);
        let count = 0;
        allFeatures.forEach((feature) => {
            if (unSavedIds.includes(feature.id)) {
                // 被修改过的，不清理
                return;
            }
            let geo;
            switch (feature.type) {
                case 'point':
                    geo = turfPoint(this.transform.project(feature.latLngs[0][0]));
                    break;
                case 'line':
                    if (feature.latLngs.length > 0) {
                        const line = feature.latLngs[0].map((latLng) => this.transform.project(latLng));
                        geo = turfLineString(line);
                    }

                    break;
                case 'polygon':
                    if (feature.latLngs.length > 0) {
                        const polygon = feature.latLngs.map((latLngsItem) => {
                            return latLngsItem.map((latLng) => this.transform.project(latLng));
                        });
                        geo = turfPolygon(polygon);
                    }

                    break;
                default:
                    break;
            }
            if (!geo) {
                return;
            }
            // 判断是否不相交，如果和地图视野范围不相交，则清理
            const disJoin = turf_booleanDisjoint(box, geo);
            if (disJoin) {
                this.removeFeature(feature.type, feature.id);
                count++;
            }
        });
        console.log('视野范围外未编辑的要素已被清除！(' + count + '个)');
    }

    /**
     * 获取所有非顶点的真实的要素
     * @returns
     */
    getAllActualFeatures(): Map<string, Feature> {
        let allFeatures: Map<string, Feature> = new Map();
        Object.values(this.layers)
            .filter((item) => item.featureType !== 'node')
            .forEach((layer) => {
                allFeatures = new Map([...allFeatures, ...layer.features]);
            });
        return allFeatures;
    }

    /**
     * 根据id获取非顶点的真实的要素
     * @param id
     * @returns 要素实例化对象或者undefined
     *  #TODO 返回类型
     */
    getActualFeature(id: string) {
        const allFeatures = this.getAllActualFeatures();
        const feature = allFeatures.get(id);
        return feature?.instance();
    }

    /**
     * 获取所有要素，包括顶点
     * @returns
     */
    getAllFeatures(): Map<string, Feature> {
        let allFeatures: Map<string, Feature> = new Map();
        Object.values(this.layers).forEach((layer) => {
            allFeatures = new Map([...allFeatures, ...layer.features]);
        });

        return allFeatures;
    }
}

export default Graphic;
